using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Practices.Unity;

namespace Sessionizer.Core.Unity
{
    public sealed class Session : ISession
    {
        private static readonly ThreadLocal<Stack<Session>> SessionStack =
            new ThreadLocal<Stack<Session>>(() => new Stack<Session>());

        private readonly IUnityContainer _container;
        private readonly ISessionStorage _storage;
        private readonly ISessionConfiguration _configuration;

        public Session(
            IUnityContainer container,
            ISessionStorage storage, 
            ISessionConfiguration configuration)
        {
            _container = container;
            _storage = storage;
            _configuration = configuration;
        }

        public static bool SessionBound
        {
            get
            {
                var stack = SessionStack.Value;
                lock (stack)
                {
                    return stack.Count > 0;
                }
            }
        }

        public static Session Current
        {
            get
            {
                var stack = SessionStack.Value;
                lock (stack)
                {
                    if (stack.Count > 0)
                    {
                        return stack.Peek();
                    }                    
                }
                throw new InvalidOperationException("There is no bound session.");
            }
        }

        public ISessionStorage Storage
        {
            get { return _storage; }
        }

        public ISessionBinding Bind()
        {
            return new Binder(this);
        }

        public void Set<TData>(TData instance)
        {
            using (Bind())
            {
                _container.SetSessions(instance);
            }
        }

        public TData Get<TData>()
        {
            using (Bind())
            {
                return _container.GetSessions<TData>();
            }
        }

        public ISessionConfiguration Configuration
        {
            get { return _configuration; }
        }

        public void Dispose()
        {
            _storage.Dispose();
        }


        private sealed class Binder : ISessionBinding
        {
            private readonly Stack<Session> _sessionStack;
            private Session _session;

            public Binder(Session session)
            {
                _sessionStack = SessionStack.Value;
                lock (_sessionStack)
                {
                    if (_sessionStack.Count != 0 && ReferenceEquals(_sessionStack.Peek(), session))
                    {
                        return;
                    }
                    _sessionStack.Push(session);
                    _session = session;
                }
            }

            public void Dispose()
            {
                if (ReferenceEquals(_session, null))
                {
                    return;
                }
                var stack = SessionStack.Value;
                if (!ReferenceEquals(_sessionStack, stack))
                {
                    throw new InvalidOperationException("Session binding release thread differs from allocation thread.");
                }
                lock (_sessionStack)
                {
                    if (!ReferenceEquals(_session, _sessionStack.Peek()))
                    {
                        throw new InvalidOperationException(
                            "Invalid session binding release order. Wrong session instance released.");
                    }

                    _sessionStack.Pop();
                    _session = null;
                }
            }
        }
    }
}